]> dgit.raspbian.org Git - gnome-shell.git/commitdiff
screenshot-ui: Add cursor capturing option
authorIvan Molodetskikh <yalterz@gmail.com>
Mon, 16 Aug 2021 15:12:05 +0000 (18:12 +0300)
committerMarge Bot <marge-bot@gnome.org>
Thu, 27 Jan 2022 22:25:42 +0000 (22:25 +0000)
The cursor texture, scale and position is captured separately and
overlaid on top of the preview, and on top of the final screenshot
image. This allows toggling it on and off post-factum.

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1954>

data/gnome-shell-theme.gresource.xml
data/theme/gnome-shell-sass/widgets/_screenshot.scss
data/theme/select-mode-symbolic.svg [new file with mode: 0644]
js/ui/screenshot.js
src/shell-screenshot.c
src/shell-screenshot.h

index 45cb9fc77f45bc5a772a33de29083c5491a8f0ed..4e4e6775794822e3705d3a353a762eaa7b618421 100644 (file)
@@ -44,5 +44,6 @@
     <file alias="icons/scalable/status/screenshot-ui-area-symbolic.svg">screenshot-ui-area-symbolic.svg</file>
     <file alias="icons/scalable/status/screenshot-ui-display-symbolic.svg">screenshot-ui-display-symbolic.svg</file>
     <file alias="icons/scalable/status/screenshot-ui-window-symbolic.svg">screenshot-ui-window-symbolic.svg</file>
+    <file alias="icons/scalable/status/select-mode-symbolic.svg">select-mode-symbolic.svg</file>
   </gresource>
 </gresources>
index 0abc0f85c243e847f361173f551136639604ba3b..87cd693edbe40675c5cf78d5046c99d0a3fe1097 100644 (file)
   }
 }
 
+.screenshot-ui-show-pointer-button {
+  padding: $base_padding * 2;
+  border-radius: 99px;
+  background-color: $hover_bg_color;
+  &:hover, &:focus { background-color: lighten($hover_bg_color, 5%); }
+  &:active { background-color: $active_bg_color; }
+  &:checked { background-color: darken($hover_bg_color, 10%); }
+
+  StIcon {
+    icon-size: $base_icon_size;
+  }
+}
+
 .screenshot-ui-area-indicator-shade {
   background-color: rgba(0, 0, 0, .3);
 }
diff --git a/data/theme/select-mode-symbolic.svg b/data/theme/select-mode-symbolic.svg
new file mode 100644 (file)
index 0000000..4b99430
--- /dev/null
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 2.953125 1.074219 l 2.417969 13.210937 l 3.238281 -2.398437 l 2.054687 2.648437 c 1.03125 1.433594 3.148438 -0.210937 2.011719 -1.5625 l -2.015625 -2.59375 l 2.984375 -2.175781 z m 0 0" fill="#2e3436"/></svg>
index 27e43c6ab46d9900af70e9a16a36e83158aa6b82..2d5785fe6b84822a5349f45016f410ef3d18d4a7 100644 (file)
@@ -726,6 +726,10 @@ class UIWindowSelectorWindow extends St.Button {
             y_align: Clutter.ActorAlign.CENTER,
         });
 
+        this._cursor = null;
+        this._cursorPoint = { x: 0, y: 0 };
+        this._shouldShowCursor = actor.get_children().some(c => c.has_pointer);
+
         this.connect('destroy', this._onDestroy.bind(this));
     }
 
@@ -753,6 +757,13 @@ class UIWindowSelectorWindow extends St.Button {
         return [0, 0];
     }
 
+    get cursorPoint() {
+        return {
+            x: this._cursorPoint.x + this._boundingBox.x - this._bufferRect.x,
+            y: this._cursorPoint.y + this._boundingBox.y - this._bufferRect.y,
+        };
+    }
+
     get bufferScale() {
         return this._bufferScale;
     }
@@ -768,6 +779,12 @@ class UIWindowSelectorWindow extends St.Button {
         this.remove_child(this._border);
         this._border.destroy();
         this._border = null;
+
+        if (this._cursor) {
+            this.remove_child(this._cursor);
+            this._cursor.destroy();
+            this._cursor = null;
+        }
     }
 
     vfunc_allocate(box) {
@@ -799,6 +816,53 @@ class UIWindowSelectorWindow extends St.Button {
             windowH * yScale / this._bufferScale
         );
         this._actor.allocate(actorBox);
+
+        // Allocate the cursor if we have one.
+        if (!this._cursor)
+            return;
+
+        let [, , w, h] = this._cursor.get_preferred_size();
+        w *= this._cursorScale;
+        h *= this._cursorScale;
+
+        const cursorBox = new Clutter.ActorBox({
+            x1: this._cursorPoint.x,
+            y1: this._cursorPoint.y,
+            x2: this._cursorPoint.x + w,
+            y2: this._cursorPoint.y + h,
+        });
+        cursorBox.x1 *= xScale;
+        cursorBox.x2 *= xScale;
+        cursorBox.y1 *= yScale;
+        cursorBox.y2 *= yScale;
+
+        this._cursor.allocate(cursorBox);
+    }
+
+    addCursorTexture(content, point, scale) {
+        if (!this._shouldShowCursor)
+            return;
+
+        // Add the cursor.
+        this._cursor = new St.Widget({
+            content,
+            request_mode: Clutter.RequestMode.CONTENT_SIZE,
+        });
+
+        this._cursorPoint = {
+            x: point.x - this._boundingBox.x,
+            y: point.y - this._boundingBox.y,
+        };
+        this._cursorScale = scale;
+
+        this.insert_child_below(this._cursor, this._border);
+    }
+
+    setCursorVisible(visible) {
+        if (!this._cursor)
+            return;
+
+        this._cursor.visible = visible;
     }
 });
 
@@ -898,6 +962,9 @@ class ScreenshotUI extends St.Widget {
         }));
         this._stageScreenshotContainer.add_child(this._stageScreenshot);
 
+        this._cursor = new St.Widget();
+        this._stageScreenshotContainer.add_child(this._cursor);
+
         this._openingCoroutineInProgress = false;
         this._grabHelper = new GrabHelper.GrabHelper(this, {
             actionMode: Shell.ActionMode.POPUP,
@@ -1007,6 +1074,30 @@ class ScreenshotUI extends St.Widget {
             this._onCaptureButtonClicked.bind(this));
         this._bottomRowContainer.add_child(this._captureButton);
 
+        this._showPointerButtonContainer = new St.BoxLayout({
+            x_align: Clutter.ActorAlign.END,
+            x_expand: true,
+        });
+        this._bottomRowContainer.add_child(this._showPointerButtonContainer);
+
+        this._showPointerButton = new St.Button({
+            style_class: 'screenshot-ui-show-pointer-button',
+            toggle_mode: true,
+        });
+        this._showPointerButton.set_child(new St.Icon({ icon_name: 'select-mode-symbolic' }));
+        this._showPointerButtonContainer.add_child(this._showPointerButton);
+
+        this._showPointerButton.connect('notify::checked', () => {
+            const state = this._showPointerButton.checked;
+            this._cursor.visible = state;
+
+            const windows =
+                this._windowSelectors.flatMap(selector => selector.windows());
+            for (const window of windows)
+                window.setCursorVisible(state);
+        });
+        this._cursor.visible = false;
+
         this._monitorBins = [];
         this._windowSelectors = [];
         this._rebuildMonitorBins();
@@ -1131,11 +1222,28 @@ class ScreenshotUI extends St.Widget {
 
             this._openingCoroutineInProgress = true;
             try {
-                const [content, scale] =
+                const [content, scale, cursorContent, cursorPoint, cursorScale] =
                     await this._shooter.screenshot_stage_to_content();
                 this._stageScreenshot.set_content(content);
                 this._scale = scale;
 
+                if (cursorContent !== null) {
+                    this._cursor.set_content(cursorContent);
+                    this._cursor.set_position(cursorPoint.x, cursorPoint.y);
+
+                    let [, w, h] = cursorContent.get_preferred_size();
+                    w *= cursorScale;
+                    h *= cursorScale;
+                    this._cursor.set_size(w, h);
+
+                    this._cursorScale = cursorScale;
+
+                    for (const window of windows) {
+                        window.addCursorTexture(cursorContent, cursorPoint, cursorScale);
+                        window.setCursorVisible(this._showPointerButton.checked);
+                    }
+                }
+
                 this._stageScreenshotContainer.show();
             } catch (e) {
                 log('Error capturing screenshot: %s'.format(e.message));
@@ -1183,6 +1291,7 @@ class ScreenshotUI extends St.Widget {
         this._stageScreenshotContainer.hide();
 
         this._stageScreenshot.set_content(null);
+        this._cursor.set_content(null);
 
         this._areaSelector.reset();
         for (const selector of this._windowSelectors)
@@ -1338,9 +1447,18 @@ class ScreenshotUI extends St.Widget {
 
             const [x, y, w, h] = this._getSelectedGeometry();
 
+            let cursorTexture = this._cursor.content?.get_texture();
+            if (!this._cursor.visible)
+                cursorTexture = null;
+
             Shell.Screenshot.composite_to_stream(
                 texture,
                 x, y, w, h,
+                this._scale,
+                cursorTexture ?? null,
+                this._cursor.x * this._scale,
+                this._cursor.y * this._scale,
+                this._cursorScale,
                 stream
             ).then(() => {
                 stream.close(null);
@@ -1370,9 +1488,18 @@ class ScreenshotUI extends St.Widget {
             const texture = content.get_texture();
             const stream = Gio.MemoryOutputStream.new_resizable();
 
+            let cursorTexture = this._cursor.content?.get_texture();
+            if (!this._cursor.visible)
+                cursorTexture = null;
+
             Shell.Screenshot.composite_to_stream(
                 texture,
                 0, 0, -1, -1,
+                window.bufferScale,
+                cursorTexture ?? null,
+                window.cursorPoint.x * window.bufferScale,
+                window.cursorPoint.y * window.bufferScale,
+                this._cursorScale,
                 stream
             ).then(() => {
                 stream.close(null);
@@ -1416,6 +1543,11 @@ class ScreenshotUI extends St.Widget {
             return Clutter.EVENT_STOP;
         }
 
+        if (symbol === Clutter.KEY_p || symbol === Clutter.KEY_P) {
+            this._showPointerButton.checked = !this._showPointerButton.checked;
+            return Clutter.EVENT_STOP;
+        }
+
         if (symbol === Clutter.KEY_Left || symbol === Clutter.KEY_Right ||
             symbol === Clutter.KEY_Up || symbol === Clutter.KEY_Down) {
             let direction;
index ac549297b5a7bc13895c3b2bca44182a6f6e67d6..f87b61e4907477b47cc932bc0aef40a4cae19597 100644 (file)
@@ -59,6 +59,9 @@ struct _ShellScreenshotPrivate
   gboolean include_frame;
 
   float scale;
+  ClutterContent *cursor_content;
+  graphene_point_t cursor_point;
+  float cursor_scale;
 };
 
 G_DEFINE_TYPE_WITH_PRIVATE (ShellScreenshot, shell_screenshot, G_TYPE_OBJECT);
@@ -305,6 +308,9 @@ grab_screenshot_content (ShellScreenshot *screenshot,
   float scale;
   g_autoptr (GError) error = NULL;
   g_autoptr (ClutterContent) content = NULL;
+  MetaCursorTracker *tracker;
+  CoglTexture *cursor_texture;
+  int cursor_hot_x, cursor_hot_y;
 
   display = shell_global_get_display (priv->global);
   meta_display_get_size (display, &width, &height);
@@ -333,6 +339,80 @@ grab_screenshot_content (ShellScreenshot *screenshot,
       return;
     }
 
+  tracker = meta_cursor_tracker_get_for_display (display);
+  cursor_texture = meta_cursor_tracker_get_sprite (tracker);
+
+  // If the cursor is invisible, the texture is NULL.
+  if (cursor_texture)
+    {
+      unsigned int width, height;
+      CoglContext *ctx;
+      CoglPipeline *pipeline;
+      CoglTexture2D *texture;
+      CoglOffscreen *offscreen;
+      ClutterStageView *view;
+
+      // Copy the texture to prevent it from changing shortly after.
+      width = cogl_texture_get_width (cursor_texture);
+      height = cogl_texture_get_height (cursor_texture);
+
+      ctx = clutter_backend_get_cogl_context (clutter_get_default_backend ());
+
+      texture = cogl_texture_2d_new_with_size (ctx, width, height);
+      offscreen = cogl_offscreen_new_with_texture (texture);
+      cogl_framebuffer_clear4f (COGL_FRAMEBUFFER (offscreen),
+                                COGL_BUFFER_BIT_COLOR,
+                                0, 0, 0, 0);
+
+      pipeline = cogl_pipeline_new (ctx);
+      cogl_pipeline_set_layer_texture (pipeline, 0, cursor_texture);
+
+      cogl_framebuffer_draw_textured_rectangle (COGL_FRAMEBUFFER (offscreen),
+                                                pipeline,
+                                                -1, 1, 1, -1,
+                                                0, 0, 1, 1);
+      cogl_object_unref (pipeline);
+      g_object_unref (offscreen);
+
+      priv->cursor_content =
+        clutter_texture_content_new_from_texture (texture, NULL);
+      cogl_object_unref (texture);
+
+      priv->cursor_scale = meta_cursor_tracker_get_scale (tracker);
+
+      meta_cursor_tracker_get_pointer (tracker, &priv->cursor_point, NULL);
+
+      view = clutter_stage_get_view_at (stage,
+                                        priv->cursor_point.x,
+                                        priv->cursor_point.y);
+
+      meta_cursor_tracker_get_hot (tracker, &cursor_hot_x, &cursor_hot_y);
+      priv->cursor_point.x -= cursor_hot_x * priv->cursor_scale;
+      priv->cursor_point.y -= cursor_hot_y * priv->cursor_scale;
+
+      // Align the coordinates to the pixel grid the same way it's done in
+      // MetaCursorRenderer.
+      if (view)
+        {
+          cairo_rectangle_int_t view_layout;
+          float view_scale;
+
+          clutter_stage_view_get_layout (view, &view_layout);
+          view_scale = clutter_stage_view_get_scale (view);
+
+          priv->cursor_point.x -= view_layout.x;
+          priv->cursor_point.y -= view_layout.y;
+
+          priv->cursor_point.x =
+              floorf (priv->cursor_point.x * view_scale) / view_scale;
+          priv->cursor_point.y =
+              floorf (priv->cursor_point.y * view_scale) / view_scale;
+
+          priv->cursor_point.x += view_layout.x;
+          priv->cursor_point.y += view_layout.y;
+        }
+    }
+
   g_task_return_pointer (result, g_steal_pointer (&content), g_object_unref);
 }
 
@@ -610,6 +690,10 @@ shell_screenshot_screenshot_stage_to_content (ShellScreenshot     *screenshot,
  * @screenshot: the #ShellScreenshot
  * @result: the #GAsyncResult that was provided to the callback
  * @scale: (out) (optional): location to store the content scale
+ * @cursor_content: (out) (optional): location to store the cursor content
+ * @cursor_point: (out) (optional): location to store the point at which to
+ * draw the cursor content
+ * @cursor_scale: (out) (optional): location to store the cursor scale
  * @error: #GError for error reporting
  *
  * Finish the asynchronous operation started by
@@ -619,10 +703,13 @@ shell_screenshot_screenshot_stage_to_content (ShellScreenshot     *screenshot,
  *
  */
 ClutterContent *
-shell_screenshot_screenshot_stage_to_content_finish (ShellScreenshot  *screenshot,
-                                                     GAsyncResult     *result,
-                                                     float            *scale,
-                                                     GError          **error)
+shell_screenshot_screenshot_stage_to_content_finish (ShellScreenshot   *screenshot,
+                                                     GAsyncResult      *result,
+                                                     float             *scale,
+                                                     ClutterContent   **cursor_content,
+                                                     graphene_point_t  *cursor_point,
+                                                     float             *cursor_scale,
+                                                     GError           **error)
 {
   ShellScreenshotPrivate *priv = screenshot->priv;
   ClutterContent *content;
@@ -640,6 +727,17 @@ shell_screenshot_screenshot_stage_to_content_finish (ShellScreenshot  *screensho
   if (scale)
     *scale = priv->scale;
 
+  if (cursor_content)
+    *cursor_content = g_steal_pointer (&priv->cursor_content);
+  else
+    g_clear_pointer (&priv->cursor_content, g_object_unref);
+
+  if (cursor_point)
+    *cursor_point = priv->cursor_point;
+
+  if (cursor_scale)
+    *cursor_scale = priv->cursor_scale;
+
   return content;
 }
 
@@ -973,6 +1071,13 @@ composite_to_stream_on_png_saved (GObject      *source,
  * @y: y coordinate of the rectangle
  * @width: width of the rectangle, or -1 to use the full texture
  * @height: height of the rectangle, or -1 to use the full texture
+ * @scale: scale of the source texture
+ * @cursor: (nullable): the cursor texture
+ * @cursor_x: x coordinate to put the cursor texture at, relative to the full
+ * source texture
+ * @cursor_y: y coordinate to put the cursor texture at, relative to the full
+ * source texture
+ * @cursor_scale: scale of the cursor texture
  * @stream: the stream to write the PNG image into
  * @callback: (scope async): function to call returning success or failure
  * @user_data: the data to pass to callback function
@@ -987,6 +1092,11 @@ shell_screenshot_composite_to_stream (CoglTexture         *texture,
                                       int                  y,
                                       int                  width,
                                       int                  height,
+                                      float                scale,
+                                      CoglTexture         *cursor,
+                                      int                  cursor_x,
+                                      int                  cursor_y,
+                                      float                cursor_scale,
                                       GOutputStream       *stream,
                                       GAsyncReadyCallback  callback,
                                       gpointer             user_data)
@@ -994,6 +1104,8 @@ shell_screenshot_composite_to_stream (CoglTexture         *texture,
   CoglContext *ctx;
   CoglTexture *sub_texture;
   cairo_surface_t *surface;
+  cairo_surface_t *cursor_surface;
+  cairo_t *cr;
   g_autoptr (GTask) task = NULL;
   g_autoptr (GdkPixbuf) pixbuf = NULL;
   g_autofree char *creation_time = NULL;
@@ -1024,6 +1136,34 @@ shell_screenshot_composite_to_stream (CoglTexture         *texture,
 
   cogl_object_unref (sub_texture);
 
+  cairo_surface_set_device_scale (surface, scale, scale);
+
+  if (cursor != NULL)
+    {
+      // Paint the cursor on top.
+      cursor_surface =
+        cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
+                                    cogl_texture_get_width (cursor),
+                                    cogl_texture_get_height (cursor));
+      cogl_texture_get_data (cursor, CLUTTER_CAIRO_FORMAT_ARGB32,
+                             cairo_image_surface_get_stride (cursor_surface),
+                             cairo_image_surface_get_data (cursor_surface));
+      cairo_surface_mark_dirty (cursor_surface);
+
+      cairo_surface_set_device_scale (cursor_surface,
+                                      1 / cursor_scale,
+                                      1 / cursor_scale);
+
+      cr = cairo_create (surface);
+      cairo_set_source_surface (cr, cursor_surface,
+                                (cursor_x - x) / scale,
+                                (cursor_y - y) / scale);
+      cairo_paint (cr);
+      cairo_destroy (cr);
+
+      cairo_surface_destroy (cursor_surface);
+    }
+
   // Save to an image.
   pixbuf = gdk_pixbuf_get_from_surface (surface,
                                         0, 0,
index a5dc96de896c0fd805cc49d9f1736aca1d988fb1..b379b8a0aa2a51915011bce009c18cf5b8616f2c 100644 (file)
@@ -53,10 +53,13 @@ gboolean shell_screenshot_screenshot_finish   (ShellScreenshot        *screensho
 void     shell_screenshot_screenshot_stage_to_content (ShellScreenshot     *screenshot,
                                                        GAsyncReadyCallback  callback,
                                                        gpointer             user_data);
-ClutterContent *shell_screenshot_screenshot_stage_to_content_finish (ShellScreenshot  *screenshot,
-                                                                     GAsyncResult     *result,
-                                                                     float            *scale,
-                                                                     GError          **error);
+ClutterContent *shell_screenshot_screenshot_stage_to_content_finish (ShellScreenshot   *screenshot,
+                                                                     GAsyncResult      *result,
+                                                                     float             *scale,
+                                                                     ClutterContent   **cursor_content,
+                                                                     graphene_point_t  *cursor_point,
+                                                                     float             *cursor_scale,
+                                                                     GError           **error);
 
 void     shell_screenshot_pick_color        (ShellScreenshot      *screenshot,
                                              int                   x,
@@ -73,6 +76,11 @@ void shell_screenshot_composite_to_stream (CoglTexture         *texture,
                                            int                  y,
                                            int                  width,
                                            int                  height,
+                                           float                scale,
+                                           CoglTexture         *cursor,
+                                           int                  cursor_x,
+                                           int                  cursor_y,
+                                           float                cursor_scale,
                                            GOutputStream       *stream,
                                            GAsyncReadyCallback  callback,
                                            gpointer             user_data);